/*
 * Copyright (C) 2011 Christopher Probst
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of the 'FoxNet Codec' nor the names of its 
 *   contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.foxnet.codec.impl;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;

/**
 * @author Christopher Probst
 */
public class ConcurrentWeakCache<K, V> {

    // The reference queue for this cache
    private static final ReferenceQueue<Object> referenceQueue =
	    new ReferenceQueue<>();

    private static final Thread queueProcessor = new Thread() {

	/*
	 * (non-Javadoc)
	 * @see java.lang.Thread#run()
	 */
	@Override
	public void run() {
	    processQueue();
	};
    };

    static {
	queueProcessor.setDaemon(true);
	queueProcessor.start();
    }

    private static void processQueue() {
	try {
	    for (;;) {

		WeakKey weakKey = (WeakKey) referenceQueue.remove();
		if (weakKey instanceof WeakValue) {
		    weakKey.classes.remove(((WeakValue) weakKey).getWeakKey());
		} else {
		    weakKey.classes.remove(weakKey);
		}
	    }
	} catch (InterruptedException e) {
	    e.printStackTrace();
	}
    }

    private static class WeakKey extends WeakReference<Object> {

	private final Map<WeakKey, WeakValue> classes;
	private final int hash;

	/**
	 * @param referent
	 */
	public WeakKey(Map<WeakKey, WeakValue> classes, Object key) {
	    super(key, referenceQueue);
	    this.classes = classes;
	    hash = key.hashCode();
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
	    return hash;
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
	    Object k = get();
	    if (obj != null && k != null) {
		return k.equals(((WeakKey) obj).get());
	    }
	    return false;
	}
    }

    private static class WeakValue extends WeakKey {

	private final WeakKey weakKey;

	public WeakValue(WeakKey weakKey, Object value) {
	    super(weakKey.classes, value);
	    this.weakKey = weakKey;
	}

	public WeakKey getWeakKey() {
	    return weakKey;
	}
    }

    // Here we store the codec types linked to their codecs
    private final Map<WeakKey, WeakValue> refs = new HashMap<>();

    // @SuppressWarnings("unchecked")
    // public V putIfAbsent(K key, V value) {
    // if (key == null) {
    // throw new NullPointerException("key");
    // }
    //
    // // Create new key
    // WeakKey newKey = new WeakKey(refs, key);
    // WeakValue newValue = new WeakValue(newKey, value);
    // WeakValue oldValue = refs.putIfAbsent(newKey, newValue);
    //
    // if (oldValue != null) {
    // return (V) oldValue.get();
    // }
    //
    // return null;
    // }

    @SuppressWarnings("unchecked")
    public V put(K key, V value) {
	if (key == null) {
	    throw new NullPointerException("key");
	}

	// Create new key
	WeakKey newKey = new WeakKey(refs, key);
	WeakValue newValue = new WeakValue(newKey, value);
	WeakValue oldValue = refs.put(newKey, newValue);

	if (oldValue != null) {
	    return (V) oldValue.get();
	}

	return null;
    }

    private static final class KeyWrapper {

	private final Object key;

	/**
	 * 
	 */
	public KeyWrapper(Object key) {
	    this.key = key;
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
	    return key.hashCode();
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
	    return key.equals(((WeakKey) obj).get());
	}
    }

    public void clear() {
	refs.clear();
    }

    public V get(K key) {
	WeakValue oldValue = refs.get(new KeyWrapper(key));
	if (oldValue != null) {
	    return (V) oldValue.get();
	}
	return null;
    }

    public V remove(K key) {
	WeakValue oldValue = refs.remove(new KeyWrapper(key));
	if (oldValue != null) {
	    return (V) oldValue.get();
	}
	return null;
    }

}
